Skip to main content

Knapsack Problems

A comprehensive guide to knapsack problem variations and dynamic programming techniques for Data Structures and Algorithms.

Table of Contents

  1. Introduction and Problem Types
  2. 0/1 Knapsack Problem
  3. Unbounded Knapsack Problem
  4. Bounded Knapsack Problem
  5. Multiple Knapsacks Problem
  6. Fractional Knapsack Problem
  7. Subset Sum Variations
  8. Coin Change Problems
  9. Advanced Knapsack Variations
  10. Optimization Techniques
  11. Usage Examples

Introduction and Problem Types

The knapsack problem is a fundamental optimization problem where we aim to maximize value while staying within weight constraints.

Problem Classification

// Basic knapsack structure
class Item {
constructor(weight, value, name = '') {
this.weight = weight;
this.value = value;
this.name = name;
this.ratio = value / weight; // Value-to-weight ratio
}
}

// Common knapsack types
const KNAPSACK_TYPES = {
ZERO_ONE: '0/1 Knapsack', // Each item can be taken 0 or 1 time
UNBOUNDED: 'Unbounded', // Each item can be taken multiple times
BOUNDED: 'Bounded', // Each item has limited quantity
MULTIPLE: 'Multiple Knapsacks', // Multiple knapsacks available
FRACTIONAL: 'Fractional' // Items can be taken partially
};

Helper Functions

// Create sample items for testing
function createItems(data) {
return data.map(([weight, value, name]) => new Item(weight, value, name));
}

// Print solution details
function printSolution(items, solution, capacity, maxValue) {
console.log(`\nKnapsack Solution (Capacity: ${capacity})`);
console.log(`Maximum Value: ${maxValue}`);
console.log('Selected Items:');

let totalWeight = 0;
let totalValue = 0;

solution.forEach((item, index) => {
if (item.taken > 0) {
console.log(` ${item.name}: weight=${item.weight}, value=${item.value}, quantity=${item.taken}`);
totalWeight += item.weight * item.taken;
totalValue += item.value * item.taken;
}
});

console.log(`Total Weight: ${totalWeight}/${capacity}`);
console.log(`Total Value: ${totalValue}`);
}

// Validate knapsack solution
function isValidSolution(items, solution, capacity) {
let totalWeight = 0;

for (let i = 0; i < solution.length; i++) {
totalWeight += items[i].weight * solution[i];
}

return totalWeight <= capacity;
}

0/1 Knapsack Problem

The classic knapsack where each item can be taken at most once.

1. Basic 0/1 Knapsack (2D DP)

function knapsack01_2D(items, capacity) {
const n = items.length;
const dp = Array.from({ length: n + 1 },
() => new Array(capacity + 1).fill(0));

// Fill the DP table
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
const currentItem = items[i - 1];

if (currentItem.weight <= w) {
// Max of taking or not taking current item
dp[i][w] = Math.max(
dp[i - 1][w], // Don't take
dp[i - 1][w - currentItem.weight] + currentItem.value // Take
);
} else {
dp[i][w] = dp[i - 1][w]; // Can't take (too heavy)
}
}
}

return dp[n][capacity];
}

Time Complexity: O(n × W) | Space Complexity: O(n × W)

2. Space-Optimized 0/1 Knapsack (1D DP)

function knapsack01_1D(items, capacity) {
const dp = new Array(capacity + 1).fill(0);

for (const item of items) {
// Traverse backwards to avoid using updated values
for (let w = capacity; w >= item.weight; w--) {
dp[w] = Math.max(dp[w], dp[w - item.weight] + item.value);
}
}

return dp[capacity];
}

Time Complexity: O(n × W) | Space Complexity: O(W)

3. 0/1 Knapsack with Item Tracking

function knapsack01WithItems(items, capacity) {
const n = items.length;
const dp = Array.from({ length: n + 1 },
() => new Array(capacity + 1).fill(0));

// Fill DP table
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
const currentItem = items[i - 1];

if (currentItem.weight <= w) {
dp[i][w] = Math.max(
dp[i - 1][w],
dp[i - 1][w - currentItem.weight] + currentItem.value
);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}

// Backtrack to find selected items
const selectedItems = [];
let w = capacity;

for (let i = n; i > 0; i--) {
if (dp[i][w] !== dp[i - 1][w]) {
selectedItems.push({
...items[i - 1],
taken: 1
});
w -= items[i - 1].weight;
} else {
selectedItems.push({
...items[i - 1],
taken: 0
});
}
}

return {
maxValue: dp[n][capacity],
items: selectedItems.reverse()
};
}

4. Recursive 0/1 Knapsack with Memoization

function knapsack01Recursive(items, capacity) {
const memo = new Map();

function solve(index, remainingCapacity) {
// Base case
if (index === items.length || remainingCapacity === 0) {
return 0;
}

const key = `${index}-${remainingCapacity}`;
if (memo.has(key)) {
return memo.get(key);
}

const currentItem = items[index];
let result;

if (currentItem.weight > remainingCapacity) {
// Can't take current item
result = solve(index + 1, remainingCapacity);
} else {
// Max of taking or not taking current item
const take = currentItem.value + solve(index + 1, remainingCapacity - currentItem.weight);
const skip = solve(index + 1, remainingCapacity);
result = Math.max(take, skip);
}

memo.set(key, result);
return result;
}

return solve(0, capacity);
}

Unbounded Knapsack Problem

Each item can be taken unlimited times.

1. Basic Unbounded Knapsack

function unboundedKnapsack(items, capacity) {
const dp = new Array(capacity + 1).fill(0);

for (let w = 1; w <= capacity; w++) {
for (const item of items) {
if (item.weight <= w) {
dp[w] = Math.max(dp[w], dp[w - item.weight] + item.value);
}
}
}

return dp[capacity];
}

Time Complexity: O(n × W) | Space Complexity: O(W)

2. Unbounded Knapsack with Item Count

function unboundedKnapsackWithCount(items, capacity) {
const dp = new Array(capacity + 1).fill(0);
const count = new Array(capacity + 1).fill().map(() => ({}));

for (let w = 1; w <= capacity; w++) {
for (let i = 0; i < items.length; i++) {
const item = items[i];

if (item.weight <= w) {
const newValue = dp[w - item.weight] + item.value;

if (newValue > dp[w]) {
dp[w] = newValue;
count[w] = { ...count[w - item.weight] };
count[w][i] = (count[w][i] || 0) + 1;
}
}
}
}

return {
maxValue: dp[capacity],
itemCount: count[capacity] || {}
};
}

3. Rod Cutting Problem (Unbounded Knapsack Variant)

function rodCutting(prices, length) {
const dp = new Array(length + 1).fill(0);
const cuts = new Array(length + 1).fill(0);

for (let i = 1; i <= length; i++) {
for (let j = 1; j <= Math.min(i, prices.length); j++) {
if (dp[i - j] + prices[j - 1] > dp[i]) {
dp[i] = dp[i - j] + prices[j - 1];
cuts[i] = j;
}
}
}

// Reconstruct solution
const solution = [];
let remaining = length;

while (remaining > 0) {
solution.push(cuts[remaining]);
remaining -= cuts[remaining];
}

return {
maxValue: dp[length],
cuts: solution
};
}

💡 Application: Rod cutting is a classic application of unbounded knapsack!


Bounded Knapsack Problem

Each item has a limited quantity available.

1. Bounded Knapsack (Multiple Items)

function boundedKnapsack(items, quantities, capacity) {
const dp = new Array(capacity + 1).fill(0);

for (let i = 0; i < items.length; i++) {
const item = items[i];
const maxCount = quantities[i];

// Process each item with its quantity limit
for (let w = capacity; w >= item.weight; w--) {
for (let count = 1; count <= maxCount && count * item.weight <= w; count++) {
dp[w] = Math.max(
dp[w],
dp[w - count * item.weight] + count * item.value
);
}
}
}

return dp[capacity];
}

2. Bounded Knapsack with Binary Lifting

function boundedKnapsackOptimized(items, quantities, capacity) {
const newItems = [];

// Convert to 0/1 knapsack using binary representation
for (let i = 0; i < items.length; i++) {
let remaining = quantities[i];
let multiplier = 1;

while (remaining > 0) {
const takeCount = Math.min(multiplier, remaining);
newItems.push({
weight: items[i].weight * takeCount,
value: items[i].value * takeCount,
name: `${items[i].name}_x${takeCount}`
});

remaining -= takeCount;
multiplier *= 2;
}
}

// Solve as 0/1 knapsack
return knapsack01_1D(newItems, capacity);
}

3. Bounded Knapsack with Item Tracking

function boundedKnapsackWithTracking(items, quantities, capacity) {
const dp = Array.from({ length: items.length + 1 },
() => new Array(capacity + 1).fill(0));

// Track the count of each item used
const usage = Array.from({ length: items.length + 1 },
() => Array.from({ length: capacity + 1 }, () => new Array(items.length).fill(0)));

for (let i = 1; i <= items.length; i++) {
const item = items[i - 1];
const maxCount = quantities[i - 1];

for (let w = 0; w <= capacity; w++) {
dp[i][w] = dp[i - 1][w]; // Don't take any
usage[i][w] = [...usage[i - 1][w]];

// Try taking different quantities
for (let count = 1; count <= maxCount && count * item.weight <= w; count++) {
const value = dp[i - 1][w - count * item.weight] + count * item.value;

if (value > dp[i][w]) {
dp[i][w] = value;
usage[i][w] = [...usage[i - 1][w - count * item.weight]];
usage[i][w][i - 1] = count;
}
}
}
}

return {
maxValue: dp[items.length][capacity],
usage: usage[items.length][capacity]
};
}

Multiple Knapsacks Problem

Multiple knapsacks with different capacities.

1. Multiple Knapsacks (Greedy Approach)

function multipleKnapsacksGreedy(items, knapsacks) {
// Sort items by value-to-weight ratio (descending)
const sortedItems = items
.map((item, index) => ({ ...item, originalIndex: index }))
.sort((a, b) => b.ratio - a.ratio);

// Sort knapsacks by capacity (descending)
const sortedKnapsacks = knapsacks
.map((capacity, index) => ({ capacity, index, items: [], value: 0, weight: 0 }))
.sort((a, b) => b.capacity - a.capacity);

// Assign items to knapsacks
for (const item of sortedItems) {
for (const knapsack of sortedKnapsacks) {
if (knapsack.weight + item.weight <= knapsack.capacity) {
knapsack.items.push(item);
knapsack.weight += item.weight;
knapsack.value += item.value;
break;
}
}
}

return sortedKnapsacks.sort((a, b) => a.index - b.index);
}

2. Multiple Knapsacks (DP Approach)

function multipleKnapsacksDP(items, knapsacks) {
const totalCapacity = knapsacks.reduce((sum, cap) => sum + cap, 0);
const k = knapsacks.length;

// dp[i][w1][w2]...[wk] = maximum value using first i items
// with weights w1, w2, ..., wk in knapsacks 1, 2, ..., k
const memo = new Map();

function solve(itemIndex, remainingCapacities) {
if (itemIndex === items.length) return 0;

const key = `${itemIndex}-${remainingCapacities.join(',')}`;
if (memo.has(key)) return memo.get(key);

let maxValue = solve(itemIndex + 1, remainingCapacities); // Skip item

// Try putting item in each knapsack
for (let knapsackIndex = 0; knapsackIndex < k; knapsackIndex++) {
const item = items[itemIndex];
if (remainingCapacities[knapsackIndex] >= item.weight) {
const newCapacities = [...remainingCapacities];
newCapacities[knapsackIndex] -= item.weight;

const value = item.value + solve(itemIndex + 1, newCapacities);
maxValue = Math.max(maxValue, value);
}
}

memo.set(key, maxValue);
return maxValue;
}

return solve(0, [...knapsacks]);
}

Fractional Knapsack Problem

Items can be taken partially (greedy solution optimal).

1. Fractional Knapsack (Greedy)

function fractionalKnapsack(items, capacity) {
// Sort by value-to-weight ratio (descending)
const sortedItems = items
.map((item, index) => ({ ...item, index }))
.sort((a, b) => b.ratio - a.ratio);

let remainingCapacity = capacity;
let totalValue = 0;
const solution = [];

for (const item of sortedItems) {
if (remainingCapacity >= item.weight) {
// Take entire item
solution.push({
...item,
fraction: 1.0,
weightTaken: item.weight,
valueTaken: item.value
});

remainingCapacity -= item.weight;
totalValue += item.value;
} else if (remainingCapacity > 0) {
// Take partial item
const fraction = remainingCapacity / item.weight;

solution.push({
...item,
fraction,
weightTaken: remainingCapacity,
valueTaken: item.value * fraction
});

totalValue += item.value * fraction;
remainingCapacity = 0;
break;
}
}

return {
maxValue: totalValue,
items: solution
};
}

Time Complexity: O(n log n) | Space Complexity: O(n)

2. Fractional vs 0/1 Comparison

function compareKnapsackTypes(items, capacity) {
const fractional = fractionalKnapsack(items, capacity);
const zeroOne = knapsack01WithItems(items, capacity);

console.log('Fractional Knapsack Result:', fractional.maxValue);
console.log('0/1 Knapsack Result:', zeroOne.maxValue);
console.log('Difference:', fractional.maxValue - zeroOne.maxValue);

return {
fractional: fractional.maxValue,
zeroOne: zeroOne.maxValue,
fractionalOptimal: fractional.maxValue >= zeroOne.maxValue
};
}

Subset Sum Variations

1. Subset Sum Problem

function subsetSum(nums, target) {
const dp = new Array(target + 1).fill(false);
dp[0] = true; // Empty subset sums to 0

for (const num of nums) {
for (let sum = target; sum >= num; sum--) {
dp[sum] = dp[sum] || dp[sum - num];
}
}

return dp[target];
}

2. Subset Sum with Count

function subsetSumCount(nums, target) {
const dp = new Array(target + 1).fill(0);
dp[0] = 1; // One way to make sum 0 (empty subset)

for (const num of nums) {
for (let sum = target; sum >= num; sum--) {
dp[sum] += dp[sum - num];
}
}

return dp[target];
}

3. Partition Problem

function canPartition(nums) {
const totalSum = nums.reduce((sum, num) => sum + num, 0);

// Can't partition if total sum is odd
if (totalSum % 2 !== 0) return false;

const target = totalSum / 2;
return subsetSum(nums, target);
}

4. Minimum Subset Sum Difference

function minimumSubsetSumDifference(nums) {
const totalSum = nums.reduce((sum, num) => sum + num, 0);
const target = Math.floor(totalSum / 2);

const dp = new Array(target + 1).fill(false);
dp[0] = true;

for (const num of nums) {
for (let sum = target; sum >= num; sum--) {
dp[sum] = dp[sum] || dp[sum - num];
}
}

// Find the largest sum ≤ target that's achievable
let maxAchievableSum = 0;
for (let i = target; i >= 0; i--) {
if (dp[i]) {
maxAchievableSum = i;
break;
}
}

return totalSum - 2 * maxAchievableSum;
}

Coin Change Problems

1. Coin Change (Minimum Coins)

function coinChange(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;

for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}

return dp[amount] === Infinity ? -1 : dp[amount];
}

2. Coin Change (Number of Ways)

function coinChangeWays(coins, amount) {
const dp = new Array(amount + 1).fill(0);
dp[0] = 1; // One way to make amount 0

for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}

return dp[amount];
}

3. Coin Change with Limited Coins

function coinChangeLimited(coins, quantities, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;

for (let i = 0; i < coins.length; i++) {
const coin = coins[i];
const maxCount = quantities[i];

for (let amt = amount; amt >= coin; amt--) {
for (let count = 1; count <= maxCount && count * coin <= amt; count++) {
if (dp[amt - count * coin] !== Infinity) {
dp[amt] = Math.min(dp[amt], dp[amt - count * coin] + count);
}
}
}
}

return dp[amount] === Infinity ? -1 : dp[amount];
}

Advanced Knapsack Variations

1. Two-Dimensional Knapsack

function twoDimensionalKnapsack(items, weightCapacity, volumeCapacity) {
const dp = Array.from({ length: weightCapacity + 1 },
() => new Array(volumeCapacity + 1).fill(0));

for (const item of items) {
for (let w = weightCapacity; w >= item.weight; w--) {
for (let v = volumeCapacity; v >= item.volume; v--) {
dp[w][v] = Math.max(
dp[w][v],
dp[w - item.weight][v - item.volume] + item.value
);
}
}
}

return dp[weightCapacity][volumeCapacity];
}

2. Knapsack with Dependencies

function knapsackWithDependencies(items, dependencies, capacity) {
const n = items.length;
const memo = new Map();

function canTake(itemIndex, taken) {
const deps = dependencies[itemIndex] || [];
return deps.every(dep => taken.has(dep));
}

function solve(index, remainingCapacity, taken) {
if (index === n) return 0;

const key = `${index}-${remainingCapacity}-${Array.from(taken).sort().join(',')}`;
if (memo.has(key)) return memo.get(key);

// Option 1: Skip current item
let maxValue = solve(index + 1, remainingCapacity, taken);

// Option 2: Take current item (if possible)
const item = items[index];
if (item.weight <= remainingCapacity && canTake(index, taken)) {
const newTaken = new Set([...taken, index]);
const value = item.value + solve(index + 1, remainingCapacity - item.weight, newTaken);
maxValue = Math.max(maxValue, value);
}

memo.set(key, maxValue);
return maxValue;
}

return solve(0, capacity, new Set());
}

3. Group Knapsack

function groupKnapsack(groups, capacity) {
const dp = new Array(capacity + 1).fill(0);

for (const group of groups) {
const newDp = [...dp];

for (const item of group.items) {
for (let w = capacity; w >= item.weight; w--) {
newDp[w] = Math.max(newDp[w], dp[w - item.weight] + item.value);
}
}

// Update dp with the best choice from this group
for (let w = 0; w <= capacity; w++) {
dp[w] = newDp[w];
}
}

return dp[capacity];
}

4. Knapsack with Conflicts

function knapsackWithConflicts(items, conflicts, capacity) {
const n = items.length;
const conflictSet = new Set(conflicts.map(([a, b]) => `${a}-${b}`));

function hasConflict(taken) {
const takenItems = Array.from(taken);
for (let i = 0; i < takenItems.length; i++) {
for (let j = i + 1; j < takenItems.length; j++) {
const pair1 = `${takenItems[i]}-${takenItems[j]}`;
const pair2 = `${takenItems[j]}-${takenItems[i]}`;
if (conflictSet.has(pair1) || conflictSet.has(pair2)) {
return true;
}
}
}
return false;
}

let maxValue = 0;

// Generate all possible subsets
for (let mask = 0; mask < (1 << n); mask++) {
const taken = new Set();
let totalWeight = 0;
let totalValue = 0;

for (let i = 0; i < n; i++) {
if (mask & (1 << i)) {
taken.add(i);
totalWeight += items[i].weight;
totalValue += items[i].value;
}
}

if (totalWeight <= capacity && !hasConflict(taken)) {
maxValue = Math.max(maxValue, totalValue);
}
}

return maxValue;
}

Optimization Techniques

1. Meet-in-the-Middle

function knapsackMeetInTheMiddle(items, capacity) {
const n = items.length;
const mid = Math.floor(n / 2);

const firstHalf = items.slice(0, mid);
const secondHalf = items.slice(mid);

// Generate all possible (weight, value) pairs for first half
function generateStates(itemList) {
const states = [];
const numItems = itemList.length;

for (let mask = 0; mask < (1 << numItems); mask++) {
let weight = 0;
let value = 0;

for (let i = 0; i < numItems; i++) {
if (mask & (1 << i)) {
weight += itemList[i].weight;
value += itemList[i].value;
}
}

if (weight <= capacity) {
states.push({ weight, value });
}
}

return states;
}

const firstStates = generateStates(firstHalf);
const secondStates = generateStates(secondHalf);

// Sort second states by weight for binary search
secondStates.sort((a, b) => a.weight - b.weight);

// For each weight, keep only the maximum value
const maxValueByWeight = new Map();
for (const state of secondStates) {
if (!maxValueByWeight.has(state.weight) ||
maxValueByWeight.get(state.weight) < state.value) {
maxValueByWeight.set(state.weight, state.value);
}
}

let maxValue = 0;

for (const firstState of firstStates) {
const remainingCapacity = capacity - firstState.weight;

// Find best matching second state
for (const [weight, value] of maxValueByWeight) {
if (weight <= remainingCapacity) {
maxValue = Math.max(maxValue, firstState.value + value);
}
}
}

return maxValue;
}

2. Branch and Bound

function knapsackBranchAndBound(items, capacity) {
// Sort items by value-to-weight ratio
const sortedItems = items
.map((item, index) => ({ ...item, index }))
.sort((a, b) => b.ratio - a.ratio);

let bestValue = 0;
let bestSolution = [];

function upperBound(nodeLevel, currentWeight, currentValue) {
let bound = currentValue;
let weight = currentWeight;
let level = nodeLevel;

// Add items greedily (fractional allowed for bound)
while (level < sortedItems.length && weight + sortedItems[level].weight <= capacity) {
weight += sortedItems[level].weight;
bound += sortedItems[level].value;
level++;
}

// Add fractional part of next item if possible
if (level < sortedItems.length) {
const remainingCapacity = capacity - weight;
bound += (remainingCapacity / sortedItems[level].weight) * sortedItems[level].value;
}

return bound;
}

function branchAndBound(level, currentWeight, currentValue, currentSolution) {
if (level === sortedItems.length) {
if (currentValue > bestValue) {
bestValue = currentValue;
bestSolution = [...currentSolution];
}
return;
}

const item = sortedItems[level];
const bound = upperBound(level, currentWeight, currentValue);

// Pruning: if bound is not better than current best, skip this branch
if (bound <= bestValue) {
return;
}

// Branch 1: Include current item (if fits)
if (currentWeight + item.weight <= capacity) {
currentSolution[item.index] = 1;
branchAndBound(
level + 1,
currentWeight + item.weight,
currentValue + item.value,
currentSolution
);
currentSolution[item.index] = 0;
}

// Branch 2: Exclude current item
branchAndBound(level + 1, currentWeight, currentValue, currentSolution);
}

const solution = new Array(items.length).fill(0);
branchAndBound(0, 0, 0, solution);

return {
maxValue: bestValue,
solution: bestSolution
};
}

3. Approximation Algorithms

function knapsackFPTAS(items, capacity, epsilon) {
const n = items.length;
const maxValue = Math.max(...items.map(item => item.value));

// Scale factor for approximation
const K = (epsilon * maxValue) / n;

// Scale and round values
const scaledItems = items.map(item => ({
...item,
scaledValue: Math.floor(item.value / K)
}));

const maxScaledValue = Math.max(...scaledItems.map(item => item.scaledValue));
const totalScaledValue = n * maxScaledValue;

// DP on scaled values
const dp = Array.from({ length: n + 1 },
() => new Array(totalScaledValue + 1).fill(Infinity));
dp[0][0] = 0;

for (let i = 1; i <= n; i++) {
const item = scaledItems[i - 1];

for (let v = 0; v <= totalScaledValue; v++) {
// Don't take item
dp[i][v] = dp[i - 1][v];

// Take item
if (v >= item.scaledValue && dp[i - 1][v - item.scaledValue] !== Infinity) {
dp[i][v] = Math.min(
dp[i][v],
dp[i - 1][v - item.scaledValue] + item.weight
);
}
}
}

// Find maximum value with weight <= capacity
let maxApproxValue = 0;
for (let v = 0; v <= totalScaledValue; v++) {
if (dp[n][v] <= capacity) {
maxApproxValue = Math.max(maxApproxValue, v * K);
}
}

return maxApproxValue;
}

Usage Examples

Here's how to use these knapsack variations:

console.log("=== Knapsack Problems Demo ===");

// Sample items for testing
const items = [
new Item(10, 60, "Item 1"),
new Item(20, 100, "Item 2"),
new Item(30, 120, "Item 3")
];

const capacity = 50;

console.log("\n1. 0/1 Knapsack Problem");
const result01 = knapsack01WithItems(items, capacity);
console.log("Max Value:", result01.maxValue);
printSolution(items, result01.items, capacity, result01.maxValue);

console.log("\n2. Unbounded Knapsack Problem");
const unboundedResult = unboundedKnapsackWithCount(items, capacity);
console.log("Max Value:", unboundedResult.maxValue);
console.log("Item counts:", unboundedResult.itemCount);

console.log("\n3. Fractional Knapsack Problem");
const fractionalResult = fractionalKnapsack(items, capacity);
console.log("Max Value:", fractionalResult.maxValue.toFixed(2));
fractionalResult.items.forEach(item => {
console.log(` ${item.name}: fraction=${item.fraction.toFixed(2)}, value=${item.valueTaken.toFixed(2)}`);
});

console.log("\n4. Multiple Knapsacks Problem");
const knapsacks = [30, 20, 25];
const multipleResult = multipleKnapsacksGreedy(items, knapsacks);
multipleResult.forEach((knapsack, index) => {
console.log(`Knapsack ${index + 1} (capacity ${knapsack.capacity}): value=${knapsack.value}`);
});

console.log("\n5. Subset Sum Problem");
const nums = [3, 34, 4, 12, 5, 2];
const target = 9;
console.log(`Can make sum ${target}:`, subsetSum(nums, target));
console.log(`Number of ways to make ${target}:`, subsetSumCount(nums, target));

console.log("\n6. Coin Change Problem");
const coins = [1, 3, 4];
const amount = 6;
console.log(`Min coins for amount ${amount}:`, coinChange(coins, amount));
console.log(`Ways to make amount ${amount}:`, coinChangeWays(coins, amount));

console.log("\n7. Rod Cutting Problem");
const prices = [1, 5, 8, 9, 10, 17, 17, 20];
const rodLength = 4;
const rodResult = rodCutting(prices, rodLength);
console.log(`Max value for rod length ${rodLength}:`, rodResult.maxValue);
console.log("Cuts:", rodResult.cuts);

console.log("\n8. Partition Problem");
const partitionNums = [1, 5, 11, 5];
console.log("Can partition into equal sum subsets:", canPartition(partitionNums));

console.log("\n9. Minimum Subset Sum Difference");
const diffNums = [1, 6, 11, 5];
console.log("Minimum difference:", minimumSubsetSumDifference(diffNums));

// Advanced examples
console.log("\n10. Two-Dimensional Knapsack");
const items2D = [
{ weight: 10, volume: 20, value: 100, name: "2D Item 1" },
{ weight: 20, volume: 30, value: 300, name: "2D Item 2" },
{ weight: 30, volume: 40, value: 400, name: "2D Item 3" }
];
const result2D = twoDimensionalKnapsack(items2D, 50, 60);
console.log("2D Knapsack max value:", result2D);

console.log("\n11. Bounded Knapsack");
const quantities = [2, 3, 1];
const boundedResult = boundedKnapsackWithTracking(items, quantities, capacity);
console.log("Bounded knapsack max value:", boundedResult.maxValue);
console.log("Item usage:", boundedResult.usage);

Time Complexity Summary

Problem TypeTime ComplexitySpace ComplexityNotes
0/1 Knapsack (2D)O(nW)O(nW)W = capacity
0/1 Knapsack (1D)O(nW)O(W)Space optimized
Unbounded KnapsackO(nW)O(W)Items reusable
Bounded KnapsackO(nWQ)O(W)Q = max quantity
Multiple KnapsacksO(n × 2^n)O(2^n)Exponential
Fractional KnapsackO(n log n)O(n)Greedy optimal
Subset SumO(nS)O(S)S = target sum
Coin Change (Min)O(nA)O(A)A = amount
Coin Change (Ways)O(nA)O(A)Order matters
Meet-in-MiddleO(n × 2^(n/2))O(2^(n/2))Space-time tradeoff
Branch & BoundO(2^n)O(n)Best case pruning
FPTASO(n³/ε)O(n²/ε)ε = approximation

Key Patterns to Remember

1. DP State Transitions

// 0/1 Knapsack transition
dp[i][w] = Math.max(
dp[i-1][w], // Don't take item
dp[i-1][w-weight[i]] + value[i] // Take item
);

// Unbounded Knapsack transition
dp[w] = Math.max(dp[w], dp[w-weight[i]] + value[i]);

2. Space Optimization

// Process weights in reverse for 0/1 knapsack
for (let w = capacity; w >= item.weight; w--) {
dp[w] = Math.max(dp[w], dp[w - item.weight] + item.value);
}

3. Greedy vs DP

  • Fractional: Greedy by value/weight ratio is optimal
  • 0/1: DP required, greedy is not optimal
  • Unbounded: DP required, but greedy gives approximation

4. Backtracking Pattern

// Reconstruct solution from DP table
let w = capacity;
for (let i = n; i > 0 && w > 0; i--) {
if (dp[i][w] !== dp[i-1][w]) {
// Item i-1 was taken
selectedItems.push(i-1);
w -= items[i-1].weight;
}
}

5. Optimization Techniques

  • Meet-in-Middle: Split items, generate all combinations
  • Branch & Bound: Use upper bounds for pruning
  • FPTAS: Scale values for polynomial approximation

Common Interview Patterns

1. Subset-Based Problems

  • Equal Sum Partition
  • Target Sum (assign +/- signs)
  • Subset Sum with specific count

2. Coin/Change Problems

  • Minimum coins needed
  • Number of ways to make change
  • Change with limited coin types

3. Optimization Variants

  • Multiple constraints (weight + volume)
  • Item dependencies
  • Group selections (choose one from each group)

4. Real-World Applications

  • Resource allocation
  • Portfolio optimization
  • Cutting stock problems
  • Task scheduling with rewards

Interview Tips

  1. Identify the variant: 0/1, unbounded, or bounded?
  2. Check constraints: Can you use space optimization?
  3. Consider approximation: For large inputs, FPTAS might be needed
  4. Greedy first: For fractional knapsack, greedy is optimal
  5. DP table design: Think about state representation
  6. Edge cases: Zero capacity, no items, negative values
  7. Optimization: Meet-in-middle for n ≤ 40, branch-and-bound for exact solutions

Essential DP Knapsack Patterns & Tricks

🎯 Core Decision Framework

Choice: Skip or Take Current Number

If elements are 0/1 choice (bounded):

// Take and move to next element (can't reuse)
const skip = dfs(i + 1, t);
const take = dfs(i + 1, t - nums[i]);

If elements are unbounded:

// Take and stay at same index (can reuse)
const skip = dfs(i + 1, t);
const take = dfs(i, t - nums[i]);

🔄 Bottom-Up DP Patterns

Knapsack 0/1 (Bounded)

function subsetSum01(nums, target) {
const dp = Array(target + 1).fill(false);
dp[0] = true; // sum 0 always possible

for (let num of nums) {
// BACKWARD loop prevents reuse of same element
for (let t = target; t >= num; t--) {
dp[t] = dp[t] || dp[t - num];
}
}
return dp[target];
}

Knapsack Unbounded

function subsetSumUnbounded(nums, target) {
const dp = Array(target + 1).fill(false);
dp[0] = true; // sum 0 always possible

for (let num of nums) {
// FORWARD loop allows reuse of same element
for (let t = num; t <= target; t++) {
dp[t] = dp[t] || dp[t - num];
}
}
return dp[target];
}

🔑 Memory Trick (Bottom-Up)

  • 0/1 (bounded) → loop t BACKWARDS → ensures each number used once
  • Unbounded → loop t FORWARDS → allows reusing same number multiple times

📊 Counting Ways: Combinations vs Permutations

Combinations (Order Doesn't Matter)

Bottom-Up:

function countUnboundedSubsetSum(nums, target) {
const dp = Array(target + 1).fill(0);
dp[0] = 1; // one way to make 0

// Outer loop nums → inner loop target = COMBINATIONS
for (let num of nums) {
for (let t = num; t <= target; t++) {
dp[t] += dp[t - num];
}
}
return dp[target];
}

console.log(countUnboundedSubsetSum([3, 4, 5], 7)); // 1 → (3+4)
console.log(countUnboundedSubsetSum([3, 4, 5], 11)); // 2 → (3+3+5), (4+4+3)
console.log(countUnboundedSubsetSum([3, 4, 5], 2)); // 0

Top-Down:

function countCombinations(nums, target) {
const memo = new Map();

function dfs(i, t) {
if (t === 0) return 1;
if (i >= nums.length || t < 0) return 0;

const key = `${i},${t}`;
if (memo.has(key)) return memo.get(key);

// Choice: skip current num OR take it (stay at i because unbounded)
let ways = dfs(i + 1, t); // skip
ways += dfs(i, t - nums[i]); // take (reuse allowed)

memo.set(key, ways);
return ways;
}

return dfs(0, target);
}

console.log(countCombinations([3, 4, 5], 7)); // 1 → {3+4}
console.log(countCombinations([3, 4, 5], 11)); // 2 → {3+3+5}, {4+4+3}

Permutations (Order Matters)

Bottom-Up:

function countPermutations(nums, target) {
const dp = Array(target + 1).fill(0);
dp[0] = 1;

// Outer loop target → inner loop nums = PERMUTATIONS
for (let t = 1; t <= target; t++) {
for (let num of nums) {
if (t >= num) {
dp[t] += dp[t - num];
}
}
}
return dp[target];
}

console.log(countPermutations([3, 4, 5], 7)); // 2 → [3,4], [4,3]
console.log(countPermutations([3, 4, 5], 11)); // 4 → [3,3,5], [3,5,3], [5,3,3], [4,4,3]

Top-Down:

function countPermutations(nums, target) {
const memo = new Map();

function dfs(t) {
if (t === 0) return 1;
if (t < 0) return 0;
if (memo.has(t)) return memo.get(t);

let ways = 0;
for (let num of nums) {
ways += dfs(t - num); // restart loop for each num
}

memo.set(t, ways);
return ways;
}

return dfs(target);
}

👉 Key Difference: No index i — we try all nums every time, so [3,4] and [4,3] are treated differently.

⚡ Loop Order Rules

  • Outer loop nums → inner loop target = COMBINATIONS
  • Outer loop target → inner loop nums = PERMUTATIONS

🔄 Mapping Bottom-Up to Top-Down

  • Combinations: Recursion keeps index i (progress through nums)
  • Permutations: Recursion loops through all nums at each step

🎒 Classic Knapsack with Weight & Value

0/1 Knapsack

function knapsack01(weights, values, capacity) {
const n = weights.length;
const dp = Array(n + 1).fill(null).map(() => Array(capacity + 1).fill(0));

for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
// Don't take item i-1
dp[i][w] = dp[i-1][w];

// Take item i-1 if it fits
if (w >= weights[i-1]) {
dp[i][w] = Math.max(dp[i][w], dp[i-1][w - weights[i-1]] + values[i-1]);
}
}
}

return dp[n][capacity];
}

Unbounded Knapsack

function knapsackUnbounded(weights, values, capacity) {
const dp = Array(capacity + 1).fill(0);

for (let w = 1; w <= capacity; w++) {
for (let i = 0; i < weights.length; i++) {
if (w >= weights[i]) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
}

return dp[capacity];
}

🔍 Additional Optimization Tricks

Space Optimization (0/1 Knapsack)

function knapsack01Optimized(weights, values, capacity) {
const dp = Array(capacity + 1).fill(0);

for (let i = 0; i < weights.length; i++) {
// BACKWARD to avoid using updated values
for (let w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}

return dp[capacity];
}

Path Reconstruction

function knapsackWithPath(weights, values, capacity) {
const n = weights.length;
const dp = Array(n + 1).fill(null).map(() => Array(capacity + 1).fill(0));

// Fill DP table
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
dp[i][w] = dp[i-1][w];
if (w >= weights[i-1]) {
dp[i][w] = Math.max(dp[i][w], dp[i-1][w - weights[i-1]] + values[i-1]);
}
}
}

// Reconstruct path
const path = [];
let i = n, w = capacity;
while (i > 0 && w > 0) {
if (dp[i][w] !== dp[i-1][w]) {
path.push(i-1);
w -= weights[i-1];
}
i--;
}

return { maxValue: dp[n][capacity], items: path.reverse() };
}

🏷️ Common DP State Patterns

Two Parameters (i, target)

// Most knapsack problems
function dfs(i, target) {
// Base cases
// Choices: skip vs take
}

One Parameter (target only)

// Unbounded problems, coin change
function dfs(target) {
// Try all possible choices
for (let choice of choices) {
// recurse with target - choice
}
}

Three Parameters (i, j, target)

// Two arrays/strings problems
function dfs(i, j, target) {
// Process both arrays simultaneously
}

💡 Problem Recognition Patterns

  • "Exact sum" → Subset sum (boolean)
  • "Number of ways" → Count combinations/permutations
  • "Maximum value with weight limit" → 0/1 or unbounded knapsack
  • "Minimum coins" → Unbounded knapsack (minimize count)
  • "Can partition" → Subset sum with target = sum/2

⚠️ Common Pitfalls

  1. Loop direction matters: Forward vs backward for bounded/unbounded
  2. Memoization key: Include all changing parameters
  3. Base cases: Handle edge cases (target=0, empty array)
  4. Integer overflow: Use appropriate data types for large sums
  5. Index bounds: Check array boundaries in recursive solutions